package com.example;



import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;


/**
 * {@code HttpClient}为HTTP请求客户端，负责将报文发送至指定URL。
 * 
 * <p>{@code HttpClient}提供了丰富的http/https请求功能，根据实际应用场景可以向服务器发送多种请求实体。</p>
 * 
 * <p>通过注解@Autowired可以注入实例对象。</p>
 * 
 * <p>使用时根据实际情况在配置文件配置:
 * 	<dl>
 * 		<dd>连接池最大连接数：httpClient.maxConnTotal，未配置默认50</dd>
 * 		<dd>每个路由最大连接数：httpClient.maxConnPerRoute，未配置默认为最大连接数的一半</dd>
 * 		<dd>超时时间(单位秒)：httpClient.timeout，未配置默认30秒</dd>
 * 		<dd>重发次数：httpClient.retryCount，未配置默认3次</dd>
 * 	</dl>
 * </p>
 * 
 * <blockquote>参数说明如下：每个路由最大连接数：httpClient.maxConnPerRoute
 * 如配置为2时，意味着请求到http://www.baidu.com的最大连接数只能为2，即使连接池还有1000个可用连接！
 * </blockquote>
 * 
 * <p><strong>设计思路：</strong>
 * HTTP请求客户端组件主要封装{@code Apache HttpClient}，为接入着提供定制化的{@code HttpClient}实例并提供向服务器发送各种格式请求的方法。
 * ​HTTP客户端组件需要满足发送{@code http}和{@code https}请求的能力，在实现过程中需要向注册器注册{@code http}工厂和{@code https}工厂。
 * ​鉴于不同服务器对请求参数编码不同，每个方法需要提供接收编码格式的参数，如果未要求编码格式，默认采用{@code UTF-8}。
 * </p>
 * 
 * @author liyuanchang
 * @since xpay-common 0.0.1
 * @date 2016.3.23
 */
@Component
public class HttpClient {
	
	
	private static final String CHARSET_UTF_8 = "UTF-8";

	private static final String CONTENT_FORM_TYPE="application/x-www-form-urlencoded";
	
	/** 连接池最大连接数 **/
	@Value("${httpClient.maxConnTotal:50}")
	private int maxConnTotal;
	
	/** 每个路由最大连接数 **/
	@Value("${httpClient.maxConnPerRoute:0}")
	private int maxConnPerRoute;
	
	/** 超时时间，秒 **/
	@Value("${httpClient.timeout:30}")
	private int timeout;
	
	/** 重发次数 **/
	@Value("${httpClient.retryCount:3}")
	private int retryCount;
	
	/** 连接最大空闲时间，超过连接最大空闲时间，连接将被清理；清理空闲连接的间隔默认与最大空闲时间相等 **/
	@Value("${httpClient.maxIdleTime:30}")
	private int maxIdleTime;
	
	/** 连接客户端 **/
	private CloseableHttpClient closeableHttpClient;
	
	/**
	 * 构造方法
	 */
	public HttpClient(){}
	
	//private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
	
	@PostConstruct
	public void init() throws Exception {
		//超时时间,单位秒
		int httpReqTimeOut = timeout * 1000;
		
		/** 
		 * maxConnPerRoute为每个路由的最大连接数，如:maxConnPerRoute=2时，
		 * 请求到www.baidu.com的最大连接数只能为2，即使连接池还有1000个可用连接！
		 */
		if (maxConnPerRoute == 0)
			maxConnPerRoute = maxConnTotal / 2;
		
		
		SSLContext sslContext = SSLContext.getInstance("TLS");
		// 初始化SSL上下文
		sslContext.init(null, new TrustManager[] { tm }, null);
		// SSL套接字连接工厂,NoopHostnameVerifier为信任所有服务器
		SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,NoopHostnameVerifier.INSTANCE);
		// 注册http套接字工厂和https套接字工厂
		Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create()
				.register("http", PlainConnectionSocketFactory.INSTANCE)
				.register("https", sslsf)
				.build();
		// 连接池管理器
		PoolingHttpClientConnectionManager pcm = new PoolingHttpClientConnectionManager(r);
		pcm.setMaxTotal(maxConnTotal);//连接池最大连接数
		pcm.setDefaultMaxPerRoute(maxConnPerRoute);//每个路由最大连接数
		/**
		 *  请求参数配置
		 *  connectionRequestTimeout:
		 *  						从连接池中获取连接的超时时间，超过该时间未拿到可用连接，
		 *  						会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
		 *  connectTimeout:
		 *  				连接上服务器(握手成功)的时间，超出该时间抛出connect timeout
		 *  socketTimeout:
		 *  				服务器返回数据(response)的时间，超过该时间抛出read timeout
		 */
		RequestConfig requestConfig = RequestConfig.custom()
				.setConnectionRequestTimeout(httpReqTimeOut)
				.setConnectTimeout(httpReqTimeOut)
				.setSocketTimeout(httpReqTimeOut)
				.build();
		/**
		 * 构造closeableHttpClient对象
		 */
		closeableHttpClient = HttpClients.custom()
				.setDefaultRequestConfig(requestConfig)
				.setConnectionManager(pcm)
				.setRetryHandler(retryHandler)
				.evictExpiredConnections()
				.evictIdleConnections(maxIdleTime, TimeUnit.SECONDS)
				.build();
		
//		this.executor.schedule(new IdleConnectionMonitor(pcm),5,TimeUnit.SECONDS);
	}
	
	/**
	 * post 数据，数据以key、value存放在Map中
	 * @param url http请求地址
	 * @param paramMap 请求参数
	 * @return 请求响应消息字符串
	 * @throws IOException 
	 * @throws ParseException 
	 * @throws ClientProtocolException 
	 * @throws Exception 
	 */
	public String postMap(String url, Map<String, String> paramMap) throws ClientProtocolException, ParseException, IOException {
		return postMap(url, paramMap, CHARSET_UTF_8);
	}
	
	/**
	 * post 数据，数据以key、value存放在Map中
	 * @param url http请求地址
	 * @param paramMap 请求参数
	 * @param charsetName 编码格式
	 * @return 请求响应消息字符串
	 * @throws IOException 
	 * @throws ParseException 
	 * @throws ClientProtocolException 
	 * @throws Exception 
	 */
	public String postMap(String url, Map<String, String> paramMap, String charsetName) throws ClientProtocolException, ParseException, IOException {
		// post请求
		HttpPost post = new HttpPost(url);
		
		// 设置参数
		List<NameValuePair> formParams = new ArrayList<NameValuePair>();
		for (Map.Entry<String, String> entry : paramMap.entrySet()) {
			formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
		}
		
		// 对参数进行编码
		post.setEntity(new UrlEncodedFormEntity(formParams, Charset.forName(charsetName)));
		
		return httpRequest(post, charsetName);
	}
	
	/**
	 * 根据约定往商户发送报文，报文属性为xmldata
	 * @param url 发送报文地址
	 * @param xmldata 发送报文的xml String
	 * @return 请求响应消息字符串
	 * @throws IOException 
	 * @deprecated 使用{@link #postMap(String, Map)}
	 */
	@Deprecated
	public String sendMerchantMsg(String url, String xmldata) throws IOException {
		return sendMerchantMsg(url, xmldata, CHARSET_UTF_8);
	}
	
	/**
	 * 根据约定往商户发送报文，报文属性为xmldata
	 * @param url 发送报文地址
	 * @param xmldata 发送报文的xml String
	 * @param charsetName 编码格式
	 * @param charsetName 编码格式
	 * @return 请求响应消息字符串
	 * @throws IOException 
	 * @deprecated 使用{@link #postMap(String, Map, String)}
	 */
	@Deprecated
	public String sendMerchantMsg(String url, String xmldata, String charsetName) throws IOException {
		// post请求
		HttpPost httpost = new HttpPost(url);
		
		// 设置参数
		List<NameValuePair> params = new ArrayList<NameValuePair>();
		params.add(new BasicNameValuePair("xmldata", xmldata));
        
        // 对参数进行编码
        httpost.setEntity(new UrlEncodedFormEntity(params, Charset.forName(charsetName)));
        
        //发起请求
        String responseText = httpRequest(httpost, charsetName);
		return responseText;
	}
	
	/**
	 * 根据与省端约定发送报文，报文格式为xmlhead和xmlbody
	 * @param url 报文地址
	 * @param xmlHead 报文头
	 * @param xmlBody 报文体
	 * @return 请求响应消息字符串
	 * @throws IOException 
	 * @throws ParseException 
	 * @throws ClientProtocolException 
	 */
	public String sendCrmMsg(String url, String xmlHead, String xmlBody) throws ClientProtocolException, ParseException, IOException {
		return sendCrmMsg(url, xmlHead, xmlBody, CHARSET_UTF_8);
	}
	
	/**
	 * 根据与省端约定发送报文，报文格式为xmlhead和xmlbody
	 * @param url 报文地址
	 * @param xmlHead 报文头
	 * @param xmlBody 报文体
	 * @param charsetName 编码格式
	 * @return 请求响应消息字符串
	 * @throws IOException 
	 * @throws ParseException 
	 * @throws ClientProtocolException 
	 */
	public String sendCrmMsg(String url, String xmlHead, String xmlBody, String charsetName) throws ClientProtocolException, ParseException, IOException {
		StringBody head = null;
		StringBody body = null;
		
		head = new StringBody(xmlHead, ContentType.create("text/plain", Charset.forName(charsetName)));
		body = new StringBody(xmlBody, ContentType.create("text/plain", Charset.forName(charsetName)));
		
		HttpEntity entity = MultipartEntityBuilder.create()
                .addPart("xmlhead", head)
                .addPart("xmlbody", body)//.setCharset(Charset.forName(charsetName))
                .build();
		
		HttpPost httpost = new HttpPost(url);
		httpost.setEntity(entity);
		
		return httpRequest(httpost, charsetName);
	}
	
	/**
	 * 发送字符串消息到指定url
	 * @param url 消息接收的url
	 * @param msg 请求消息 
	 * @return 请求响应消息字符串
	 * @throws IOException 
	 * @throws ParseException 
	 * @throws ClientProtocolException 
	 */
	public String sendStringMsg(String url, String msg) throws ClientProtocolException, ParseException, IOException {
		return sendStringMsg(url, msg, CHARSET_UTF_8,null);
	}
	
	/**
	 * 发送字符串消息到指定url
	 * @param url  消息接收的url
	 * @param msg  请求消息
	 * @param charsetName 编码格式
	 * @param contentType ContentType格式
	 * @return  请求响应消息字符串
	 * @throws ClientProtocolException
	 * @throws ParseException
	 * @throws IOException
	 */
	public String sendStringMsg(String url, String msg, String charsetName,String contentType) throws ClientProtocolException, ParseException, IOException {
		//字符串Entity
		StringEntity entity = null;
		entity = new StringEntity(msg, Charset.forName(charsetName));
		//
		if(!StringUtils.isEmpty(contentType)){
			entity.setContentType(contentType);
		}
		//http post请求
		HttpPost httpost = new HttpPost(url);
		httpost.setEntity(entity);

		return httpRequest(httpost, charsetName);
	}


	/**
	 *
	 * @param url 消息接收的url
	 * @param msg 请求消息 消息为key=value&key2=val2形式
	 * @return
	 * @throws ClientProtocolException
	 * @throws ParseException
	 * @throws IOException
	 */
	public String sendFormMsg(String url, String msg) throws ClientProtocolException, ParseException, IOException {
		return sendStringMsg(url, msg, CHARSET_UTF_8,CONTENT_FORM_TYPE);
	}


	/**
	 * http 请求
	 * @param request
	 * @return 请求响应消息字符串
	 * @throws ClientProtocolExceptio
	 * @throws IOException 
	 * @throws ParseException 
	 */
	private String httpRequest(HttpUriRequest request, String charsetName)
			throws ClientProtocolException, ParseException, IOException {
		String responseText = null;
		CloseableHttpResponse response = null;
		HttpEntity entitys = null;
		try {
			response = closeableHttpClient.execute(request);
			if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
				entitys = response.getEntity();
				if (entitys != null) {
					// 将返回实体转换为字符串
					responseText = EntityUtils.toString(entitys, Charset.forName(charsetName));
				}
			} else {
				// 放弃连接
				request.abort();
			}
		} catch (ClientProtocolException e) {
			throw e;
		} catch (ParseException e) {
			throw e;
		} catch (IOException e) {
			throw e;
		} finally {
			if (entitys != null) {
				try {
					//释放资源可用触发连接放回连接池
					EntityUtils.consume(entitys);
				} catch (IOException e) {
					throw e;
				}
        	}
		}
		return responseText;
	}
	
	/**
	 * 重发处理器
	 */
	private HttpRequestRetryHandler retryHandler = new HttpRequestRetryHandler() {

		@Override
		public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
			// 打印警告信息
//			logger.warn(String.format("HttpClient向%s发起请求失败，出现异常%s，第%d次进入重发处理器判断是否需要重发。", context.getAttribute(HttpCoreContext.HTTP_TARGET_HOST), exception,executionCount));
			if (executionCount >= retryCount) {
				// Do not retry if over max retry count
				return false;
			}
			if (exception instanceof ConnectTimeoutException) {
				// Connection refused
				return true;
			}
			if (exception instanceof NoHttpResponseException) {
				// failed to respond
				return true;
			}
			if (exception instanceof UnknownHostException) {
				// Unknown host
				return true;
			}
			if (exception instanceof InterruptedIOException) {
				// Timeout
				return true;
			}
			if (exception instanceof SSLException) {
				// SSL handshake exception
				return false;
			}
			HttpClientContext clientContext = HttpClientContext.adapt(context);
			HttpRequest request = clientContext.getRequest();
			boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
			if (idempotent) {
				// 必须是幂等性的才能进行重发
				return true;
			}
			return false;
		}
	};
	
	/**
	 * 信任管理器
	 */
	private static X509TrustManager tm = new X509TrustManager() {
		@Override
		public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
		}

		@Override
		public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
		}

		@Override
		public X509Certificate[] getAcceptedIssuers() {
			return null;
		}
	};

	public int getMaxConnTotal() {
		return maxConnTotal;
	}

	public void setMaxConnTotal(int maxConnTotal) {
		this.maxConnTotal = maxConnTotal;
	}

	public int getMaxConnPerRoute() {
		return maxConnPerRoute;
	}

	public void setMaxConnPerRoute(int maxConnPerRoute) {
		this.maxConnPerRoute = maxConnPerRoute;
	}

	public int getTimeout() {
		return timeout;
	}

	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	public int getRetryCount() {
		return retryCount;
	}

	public void setRetryCount(int retryCount) {
		this.retryCount = retryCount;
	}

	public static class IdleConnectionMonitor implements Runnable{
		private final HttpClientConnectionManager connMgr;  
		private volatile boolean shutdown;  
	    public IdleConnectionMonitor(HttpClientConnectionManager connMgr) {  
	        this.connMgr = connMgr;  
	    }  
		@Override
		public void run() {
            while (!shutdown) {  
                synchronized (this) {  
//		                    wait(5000);  
                    // Close expired connections  
                    connMgr.closeExpiredConnections();  
                    // Optionally, close connections  
                    // that have been idle longer than 30 sec  
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);  
                }  
            }  
		}
		
		public void shutdown() {  
	        shutdown = true;  
	        synchronized (this) {  
	            notifyAll();  
	        }  
	    } 
		
	}
}
